﻿/**
 * 作者 fengxingguoyuan
 * Email wnf0000@qq.com
 * **/
(function () {

    function raiseEvent(eventType, data) {
        var event = new CustomEvent(eventType, {
            detail: data,
            bubbles: true,
            cancelable: true,
        });
        document.dispatchEvent(event);
    }
    function defineReadonlyProperty(obj, name, value) {
        Object.defineProperty(obj, name, {
            value: value,
            writable: false,
            enumerable: true,
            configurable: true
        });
    }
    function addCommand(command) {
        if (command.name in shortcut) {
            let reserved = [];
            for (n in shortcut)
                reserved.push(n);
            throw "名称name为" + command.name + "的快捷方式已经存在或该名称为保留名称！请先删除或者使用别的名称。已占用名称：" + reserved.join(',');
        }

        for (var k of command.keySet) {
            if (!k.isCmdKey && !(k.key in processkeys)) {
                if (k.key.length > 1) throw "{" + k.key + "}不是有效的普通键";
            }

        }

        defineReadonlyProperty(shortcut.eventType, command.name, command.eventType);
        defineReadonlyProperty(shortcut, command.name, command);
        commands.push(command);
        return command;
    }
    function addCommandTosubset(command) {
        if (command.name in shortcut || command.name in shortcut.subset) {
            let reserved = [];
            for (n in shortcut)
                reserved.push(n);
            for (n in shortcut.subset)
                reserved.push(n);
            throw "名称name为" + command.name + "的快捷方式已经存在或该名称为保留名称！请先删除或者使用别的名称。已占用名称：" + reserved.join(',');
        }
        for (var k of command.keySet) {
            if (!k.isCmdKey && !(k.key in processkeys)) {
                if (k.key.length > 1) throw "{" + k.key + "}不是有效的普通键";
            }

        }

        defineReadonlyProperty(shortcut.subset.eventType, command.name, command.eventType);
        defineReadonlyProperty(shortcut.subset, command.name, command);
        commands.push(command);
        return command;
    }

    const combinNormalKeys = (() => {
        const keys = "0123456789abcdefghijklmnopqrstuvwxyz";
        return keys.split('').reverse().reduce((result, a) => {
            return keys.split('').reduce((array, b) => {
                array.push([a, b])
                return array;
            }, result)
        }, [])
    })();
    const alternateKeys = (() => {
        key = `\`1234567890-=\\][';/.,`.split('');
        alt = `~!@#$%^&*()_+|}{":?><`.split('');
        let withShift = {};
        key.forEach((k, i) => withShift[k] = alt[i].charCodeAt());
        let noShift = {};
        alt.forEach((k, i) => noShift[k] = key[i].charCodeAt());

        return { withShift, noShift };
    })();
    
    const supportKeys = (() => {
        return `0123456789abcdefghijklmnopqrstuvwxyz\`-=\][';/.,`.split("").map(k => [k]).concat(combinNormalKeys);
    })();
    const shortcut = {
        getNormalKey: function (index) {
            return supportKeys[index];
        },
        getCombinNormalKey: function (index) {
            return combinNormalKeys[index];
        },
        addShortcut: function (name, cmdKeys, normalKey) {
            return addCommand(new Command(name, cmdKeys, normalKey));
        },
        addSubsetShortcut: function (name, cmdKeys, normalKey) {
            return addCommandTosubset(new Command(name, cmdKeys, normalKey));
        },
        addOrReplaceSubsetShortcut: function (name, cmdKeys, normalKey) {
            this.removeSubsetShortcut(name);
            return addCommandTosubset(new Command(name, cmdKeys, normalKey));
        },
        removeShortcut: function (name) {
            var cmd = commands.find(cmd => cmd.name == name);
            if (cmd) {
                const index = commands.indexOf(cmd);
                commands.splice(index, 1);
                delete shortcut[name];
                delete shortcut.eventType[name];
                delete shortcut.subset[name];
                delete shortcut.subset.eventType[name];
            }
        },
        removeSubsetShortcut: function (name) {
            var cmd = commands.find(cmd => cmd.name == name);
            if (cmd) {
                const index = commands.indexOf(cmd);
                commands.splice(index, 1);
                delete shortcut.subset[name];
                delete shortcut.subset.eventType[name];
            }
        },
        addEventListenerByName: function (name, listener) {
            if (typeof listener != "function") throw listener.name + "应为function";
            var command = commands.find(cmd => cmd.name == name)
            if (command) {
                document.addEventListener(command.eventType, listener);
            }
            return listener;
        },
        removeEventListenerByName: function (name, listener) {
            if (typeof listener != "function") throw listener.name + "应为function";
            var command = commands.find(cmd => cmd.name == name)
            if (command) {
                document.removeEventListener(command.eventType, listener);
            }
        },
        all: () => commands.map(cmd => { return { name: cmd.name, key: cmd.key } }),

    };
    defineReadonlyProperty(shortcut, "eventType", {});
    defineReadonlyProperty(shortcut, "subset", {});
    defineReadonlyProperty(shortcut.subset, "eventType", {});
    let namedCmdkeys = {
        alt: { key: "alt", keyCode: 18 },
        ctrl: { key: "ctrl", keyCode: 17 },
        shift: { key: "shift", keyCode: 16 },
        meta: { key: "meta", keyCode: 91 }
    };
    let processkeys = {
        arrowleft: { key: "arrowleft", keyCode: 37,alias:"◁" },
        arrowright: { key: "arrowright", keyCode: 39, alias: "▷" },
        arrowup: { key: "arrowup", keyCode: 38, alias:"△" },
        arrowdown: { key: "arrowdown", keyCode: 40,alias:"▽" }
    };
    let cmdKeyObject = {};
    for (const key in namedCmdkeys) cmdKeyObject[key] = key;
    defineReadonlyProperty(shortcut, "cmdKey", cmdKeyObject);

    var CustomKey = function (withCtrl, withAlt, withShift, withMeta, key) {
        key = key.toLowerCase();
        if (key == "control") key = "ctrl";
        let keyCode = -1;
        let code4 = -1;
        let cmdKey = namedCmdkeys[key];
        if (cmdKey) {
            keyCode = cmdKey.keyCode;
            code4 = cmdKey.keyCode;
        }
        else if (key in processkeys) {
            keyCode = processkeys[key].keyCode;
            code4 = keyCode;
            key = processkeys[key].alias;
        }
        else {
            keyCode = key.charCodeAt(0);
            if (withShift && key in alternateKeys.withShift) {
                code4 = alternateKeys.withShift[key];
            }
            else if (!withShift && key in alternateKeys.noShift) {
                code4 = alternateKeys.noShift[key];
            }
            else {
                code4 = keyCode;
            }
            
        }

        this.key = key;
        this.keyCode = keyCode;
        this.code4 = code4;
        this.withCtrl = withCtrl;
        this.withAlt = withAlt;
        this.withShift = withShift;
        this.withMeta = withMeta;
        this.withCmdKey = this.withCtrl || this.withAlt || this.withShift || this.withMeta;
        this.isCmdKey = this.keyCode == 17 || this.keyCode == 18 || this.keyCode == 16 || this.keyCode == 91;

        this.equals = function (customKey) {

            return (this.withAlt == customKey.withAlt
                && this.withCtrl == customKey.withCtrl
                && this.withShift == customKey.withShift
                && this.withMeta == customKey.withMeta
                ///&& this.key == customKey.key
                && this.code4 == customKey.code4);
        }
    }

    var Command = function (name, cmdKeys, normalKeys) {
        if (!(cmdKeys instanceof Array)) {
            cmdKeys = cmdKeys.toString().toLowerCase().split(',');
        }
        if (!(normalKeys instanceof Array)) {
            normalKeys = normalKeys.toString().toLowerCase().split('');
        }
        defineReadonlyProperty(this, "name", name);
        defineReadonlyProperty(this, "normalKeys", normalKeys);
        defineReadonlyProperty(this, "keySet", (() => {
            var keyset = [];
            let withCtrl = false; let withAlt = false; let withShift = false; let withMeta = false;
            for (let key of cmdKeys) {
                key = key.toLowerCase();
                withCtrl = withCtrl || key == 'ctrl';
                withAlt = withAlt || key == 'alt';
                withShift = withShift || key == 'shift';
                withMeta = withMeta || key == 'meta';
            }
            for (let key of cmdKeys.concat(normalKeys)) {
                key = key.toLowerCase();
                keyset.push(new CustomKey(withCtrl, withAlt, withShift, withMeta, key));
            }
            return keyset;
        })());
        defineReadonlyProperty(this, "eventType", "eventType_" + name);
        defineReadonlyProperty(this, "key", this.keySet.map(k =>k.key).join('+'));
        defineReadonlyProperty(this, "keyu", this.key.toUpperCase());
        defineReadonlyProperty(this, "keyq", "(" + this.key + ")");
        defineReadonlyProperty(this, "keyqu", this.keyq.toUpperCase());
        defineReadonlyProperty(this, "listeners", { index: 0 });
        this.on = undefined;
        this.toString = function (format) {
            let key = this.key;
            if (format && typeof format == "string") {
                format = format.toLowerCase();
                if (format.indexOf("q") > -1) {
                    key = this.keyq;
                }
                if (format.indexOf("u") > -1) {
                    key = key.toUpperCase();
                }
            }
            return key
        }
        this.addListener = function (func) {
            if (typeof func != "function") throw "func必须为function对象";
            this.listeners[this.listeners.index] = func;
            return this.listeners.index++;
        }
        this.removeListener = function (listenerId) {
            delete this.listeners[listenerId];
        }
        this.destroy = function () {
            shortcut.removeShortcut(this.name);
        }
        this.withOn = function (func) {
            this.on = func;
            return this;
        }
    };

    const commands = [];
    const presetCommands = [
        new Command("cancel", [shortcut.cmdKey.ctrl], ["Q"]),
        new Command("save", [shortcut.cmdKey.ctrl], ["S"]),
        new Command("reset", [shortcut.cmdKey.ctrl], ["Z"]),
        new Command("query", [shortcut.cmdKey.ctrl], ["M"]),
        new Command("create", [shortcut.cmdKey.ctrl], "N"),
        new Command("plus", [shortcut.cmdKey.alt], "P"),
        new Command("minus", [shortcut.cmdKey.alt], "M")
    ];
    presetCommands.forEach(command => addCommand(command));
    presetCommands.slice(0);

    let customKeys = [];
    let lastCmdkeys = [];
    let timeOutNumberMatch = 0;
    let matchedCommands = [];
    window.onkeydown = function () {
        let customKey = new CustomKey(event.ctrlKey, event.altKey, event.shiftKey, event.metaKey, event.key, event.keyCode);
        if (!customKey.withCmdKey) {
            //如果没有按住命令键视为无效键
            customKeys = [];
            return;
        }
        if (customKey.isCmdKey) {
            //如果是命令键
            for (k of customKeys.filter(s => s.isCmdKey)) {
                k.withAlt = customKey.withAlt;
                k.withCtrl = customKey.withCtrl;
                k.withShift = customKey.withShift;
                k.withMeta = customKey.withMeta;
            }
            //判断是否已经在按键集合里了
            if (customKeys.findIndex(k => k.equals(customKey)) == -1) {
                //不存在则加进集合中
                customKeys.push(customKey);
            }
        }
        else {
            if (
                (customKey.withCtrl && customKeys.findIndex(k => k.withCtrl) == -1)
                || (customKey.withAlt && customKeys.findIndex(k => k.withAlt) == -1)
                || (customKey.withShift && customKeys.findIndex(k => k.withShift) == -1)
                || (customKey.withMeta && customKeys.findIndex(k => k.withMeta) == -1)
            ) {
                lastCmdkeys.forEach(k => customKeys.push(k));
            }
            customKeys.push(customKey);
        }

        var match = false;
        //进行比对
        for (const command of commands) {
            if (command.keySet.length == customKeys.length && command.keySet.every((k, i) => k.equals(customKeys[i]))) {
                //匹配上
                match = true;
                var cleareds = matchedCommands.reduce((array, m) => {
                    if (m.cleared == true) array.push(m)
                    return array;
                }, []);
                for (let c of cleareds) {
                   
                    let index = matchedCommands.findIndex(m => m.timeoutId == c.timeoutId);
                    if (index > -1) {
                        matchedCommands.splice(index, 1);
                    }
                }
                for (var timeoutCmd of matchedCommands) {
                    if (timeoutCmd.command.keySet.length < command.keySet.length) {
                        clearTimeout(timeoutCmd.timeoutId);
                        timeoutCmd.cleared = true;
                    }
                }

                var timeoutId = setTimeout(() => {
                    matchedCommands.find(m => m.timeoutId == timeoutId).cleared = true;
                    lastCmdkeys = customKeys.filter(k => k.isCmdKey);
                    customKeys = [];
                    try {
                        if (typeof command.on == "function")
                            command.on(command);
                    }
                    catch (e) {
                        console.error(e)
                    }
                    raiseEvent(command.eventType, command);

                    for (let func in command.listeners) {
                        if (typeof command.listeners[func] != "function") continue;
                        try {
                            command.listeners[func](command);
                        }
                        catch (e) {
                            console.error(e)
                        }
                    }
                }, 500);
                matchedCommands.push({ timeoutId, command });
            }

        }
        //对比结束
        if (match) {
            //匹配上了
            if (timeOutNumberMatch != 0) {
                clearTimeout(timeOutNumberMatch)
            }
            event.preventDefault();
        }
        else {
            //没匹配上
            if (timeOutNumberMatch != 0) {
                clearTimeout(timeOutNumberMatch)
            }
            //2秒没有下一次案件则清空按键集合
            timeOutNumberMatch = setTimeout(() => {
                customKeys = [];
            }, 2000);
        }
    };

    window.shortcut = shortcut;
})();